<?php
namespace Tlf;
/**
* Base class for test classes
* @note main class file executes tests. Traits contain everything you use INSIDE a test
*/
class Tester {
use Tester\Assertions;
use Tester\Exceptions;
use Tester\Databasing;
use Tester\Utilities;
use Tester\Server;
use Tester\Other;
protected $catchers = [];
/**
* Comparisons from a single test. Should be reset between tests.
*/
protected $assertions = ['pass'=> 0, 'fail'=>0];
protected $enabled = true;
protected $options = [];
/**
* The cli class used to run the tests
*/
public $cli = null;
/**
* The string name of the method being called for the current test. Like `"testSomething"`
*/
public ?string $current_test = null;
/**
* @param $options usually args passed from the command line.
*/
public function __construct(array $options=[], $cli=null){
$this->backward_compatability();
if ($cli==null)$this->options = $options;
else $this->options = &$cli->args;//$options;
//$this->options
if ($this->options['set_error_handler']??true){
set_error_handler([$this,'throwError']);
}
$this->cli = $cli;
if (!isset($this->options['test']))$this->options['test'] = [];
if (!isset($this->options['class']))$this->options['class'] = [];
}
public function throwError($errno, $errstr, $errfile, $errline) {
throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
}
/** @beta(may 4, 2022)
* @param $test_name the name of the test (the portion of the method name after `test`)
*/
public function onBeforeTest(){}
/**
* @deprecated This method will be removed in v0.4
*/
public function backward_compatability(){}
/**
* Get array of test methods names
* @return array like `['testMethodOne', 'testMethodTwo']`
*/
public function get_test_methods(){
$list = [];
foreach (get_class_methods($this) as $method){
if ($method=='test')continue;
if (substr($method,0,4)!='test')continue;
$list[] = $method;
}
// @TODO allow multiple 'test' params to be passed. I think this worked previously, but now it's broken.
//@bugfix args['test'] is supposed to be an array ... but I think I broke it at some point. So this is_string() check is to convert it to an array
if (is_string($this->cli->args['test']))$this->cli->args['test'] = [$this->cli->args['test']];
if (count($this->cli->args['test'])>0){
$tests = array_flip($this->cli->args['test']);
$list = array_filter($list,
function($test_name) use ($tests){
return isset($tests[substr($test_name,4)]);
}
);
}
return $list;
}
/**
* get a readable name from a test method name
*/
public function get_test_name($method_name): string{
$name = $method_name;
if (substr($method_name,0,4)=='test')$name = substr($method_name,4);
return $name;
}
public function run_test_method($method){
$name = $this->get_test_name($method);
$test =
[
'method'=>$method,
'error'=>null,
'name'=>$name,
'pass'=>false,
'enabled'=>$this->enabled,
];
$this->onBeforeTest($name);
$this->assertions = ['pass'=>0, 'fail'=>0];
$this->catchers = [];
$bench_start = microtime(true);
$ob_level = $this->startOb();
try {
$this->current_test = $method;
$this->$method();
} catch (\Throwable $t){
$test['error'] = $t->__toString();
echo $test['error'];
}
$this->current_test = null;
$test['enabled'] = $this->enabled;
$test['assertions'] = $this->assertions;
$test['output'] = $this->endOb($ob_level);
$test['bench'] = $this->benchEnd($bench_start);
if ($this->assertions['pass']>=1
&&$this->assertions['fail']===0
&&$test['error'] === null
){
$test['pass'] = true;
}
return $test;
}
public function print_test_results($test){
$status = $test['pass'] ? 'PASS' : 'FAIL';
$symbol = $test['pass'] ? "\033[0;32m++\033[0m" : "\033[0;31m--\033[0m";
// print_r($test);
// exit;
if (in_array($test['name'], $this->options['test'])){
if ($test['enabled']!=true)$symbol = '/';
$str = str_repeat($symbol, 15);
echo "\n$str ".$test['name']."[start] $str\n";
echo $test['output'];
if (($c=count($this->catchers))>0){
echo "\n\n EXCEPTION FAIL:{$c} exceptions were not handled.";
}
$class = get_class($this);
echo "\n$str ".$test['name']."[end] ($class) $str";
return;
}
if ($test['enabled']!=true)$symbol = '//';
// $assertions =
// '+'.$test['assertions']['pass']
// .', -'.$test['assertions']['fail'];
// ;
$bench = '';
if ($test['bench']['diff']>$this->options['bench.threshold']){
$ms = $test['bench']['diff'] * 1000;
$ms = number_format($ms,4);
$bench=' '.$ms.'ms';
}
$assertions = '';
if ($test['pass']){
$assertions = " (+".$test['assertions']['pass'].')';
} else {
$assertions = " (+".$test['assertions']['pass'].", -".$test['assertions']['fail'].')';
}
echo "\n $symbol ".$test['name']. $bench . $assertions; //." ($assertions)";
}
/**
* Run tests
*
* @param $methods an array of method names to run as tests or NULL to run all methods beginning with 'test'
*/
public function run(){
$class = explode('\\',get_class($this));
$name = array_pop($class);
echo "\n". array_pop($class).'\\'.$name.': ';
$methods = $this->get_test_methods();
$results = [
'class'=>get_class($this),
'tests_run'=>0,
'pass'=>0,
'fail'=>0,
'disabled'=>0,
'assert_pass'=>0,
'assert_fail'=>0,
'failed_tests'=>[],
];
$tests = [];
if (count($methods)==0)return $results;
$this->prepare();
foreach ($methods as $method){
$this->inverted = false;
$this->enabled = true;
try {
$this->will_run_test($method);
} catch (\Exception $e){
$class = get_class($this);
echo " $class::will_run_test() threw exception."; // for test method '$method' on class '$class'";
if (isset($this->cli->args['test'])&& !empty($this->cli->args['test'])){
echo "\nException: ".$e->getMessage();
echo "\nStackTrace: ". $e->getTraceAsString();
}
continue;
}
$test = $this->run_test_method($method);
try {
$this->did_run_test($method, $test);
} catch (\Exception $e){
$class = get_class($this);
echo " $class::did_run_test() threw exception"; // for test method '$method' on class '$class'";
if (isset($this->cli->args['test']) && !empty($this->cli->args['test'])){
echo "\nException: ".$e->getMessage();
echo "\nStackTrace: ". $e->getTraceAsString();
}
}
if ($test===false){
continue;
}
$this->print_test_results($test);
$tests[] = $test;
$results['tests_run']++;
if ($test['enabled']!==true){
$results['disabled']++;
} else if ($test['pass']===true){
$results['pass']++;
} else {
$results['failed_tests'][] = $method;
$results['fail']++;
}
$results['assert_pass']+=$test['assertions']['pass'];
$results['assert_fail']+=$test['assertions']['fail'];
// $test['enabled'] = $this->enabled;
// print_r($test['assertions']);
// exit;
}
$this->finish();
// echo "\n ".$results['fail'].' fail, '.$results['pass'].' pass';;
// $results['tests'] = $tests;
// $results = $this->assertions;
// print_r($results);
// exit;
//
// print_r($this->assertions);
// exit;
return $results;
}
/**
* @param $start_time a value from `microtime(true)`
*/
public function benchEnd($start_time){
$end = microtime(true);
$diff = $end - $start_time;
return [
'start'=>$start_time,
'end'=>$end,
'diff'=>$diff
];
}
/**
* @override to execute before a single test runs
*/
public function will_run_test(string $method_name){}
/**
* @override to execute after a single test runs
*/
public function did_run_test(string $method_name, array $test_result){}
}